﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using Logistics.Web.Models.Common;
using Logistics.Web.Models.Track;
using Logistics.Web.Utilities;
using Nop.Core;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Directory;
using Nop.Core.Domain.Logistics;
using Nop.Core.Domain.Shipping;
using Nop.Services.Common;
using Nop.Services.Customers;
using Nop.Services.Helpers;
using Nop.Services.Localization;
using Nop.Services.Logging;
using Nop.Services.Logistics;
using Nop.Services.Orders;
using Nop.Services.Security;
using Nop.Services.Shipping;
using Nop.Services.Shipping.Tracking;
using Nop.Web.Framework;

namespace Logistics.Web.Controllers
{
    public class TrackController : BaseController
    {
        #region Fields

        private readonly ILogisticsService _logisticsService;
        private readonly ILogisticsProcessingService _logisticsProcessingService;
        private readonly IPdfService _pdfService;
        private readonly IWorkContext _workContext;
        private readonly IGenericAttributeService _genericAttributeService;
        private readonly IPermissionService _permissionService;
        private readonly IDateTimeHelper _dateTimeHelper;
        private readonly Nop.Services.Orders.IOrderProcessingService _orderProcessingService;
        private readonly IOrderService _orderService;
        private readonly ILocalizationService _localizationService;
        private readonly LogisticsInformationSettings _logisticsInformationSettings;
        private readonly ICustomerService _customerService;
        private readonly IAddressService _addressService;
        private readonly IShippingService _shippingService;
        private readonly IShipmentService _shipmentService;
        private readonly ILogger _logger;

        #endregion

        #region Ctor

        public TrackController(
            ILogisticsService logisticsService,
            ILogisticsProcessingService logisticsProcessingService,
            IGenericAttributeService genericAttributeService,
            IPdfService pdfService,
            IPermissionService permissionService,
            Nop.Services.Orders.IOrderProcessingService orderProcessingService,
            IOrderService orderService,
            IWorkContext workContext,
            IDateTimeHelper dateTimeHelper,
            LogisticsInformationSettings logisticsInformationSettings,
            ILocalizationService localizationService,
            ICustomerService customerService,
            IAddressService addressService,
            IShippingService shippingService,
            IShipmentService shipmentService,
            ILogger logger)
        {
            this._logisticsService = logisticsService;
            this._logisticsProcessingService = logisticsProcessingService;
            this._genericAttributeService = genericAttributeService;
            this._pdfService = pdfService;
            this._permissionService = permissionService;
            this._orderProcessingService = orderProcessingService;
            this._orderService = orderService;
            this._workContext = workContext;
            this._dateTimeHelper = dateTimeHelper;
            this._localizationService = localizationService;
            this._logisticsInformationSettings = logisticsInformationSettings;
            this._customerService = customerService;
            this._addressService = addressService;
            this._shippingService = shippingService;
            this._shipmentService = shipmentService;
            this._logger = logger;
        }

        #endregion

        #region Methods

        public ActionResult Status(int id)
        {
            if (!_permissionService.Authorize(StandardPermissionProvider.ViewLogisticsRoutes))
                return new HttpUnauthorizedResult();

            var customer = _workContext.CurrentCustomer;

            // retrieve package
            var pkg = _logisticsService.GetPackageById(id);
            if (pkg == null)
            {
                return Content(String.Format("ID is invalid: {0}", id));
            }

            // IF viewed by courier: determines next state (based on current state)
            var plan = _logisticsService.PackageLatestPlan(pkg);
            if (plan == null)
            {
                return Content(String.Format("The package (ID: {0}) is not in the plan.", id));
            }

            int planId = plan.Id;

            bool canManageLogistics = _permissionService.Authorize(StandardPermissionProvider.ManageLogistics);
            bool canManageWarehouse = _permissionService.Authorize(StandardPermissionProvider.ManageWarehouse) || canManageLogistics;

            if ((customer.Id != plan.Courier.Customer.Id) &&
                !canManageLogistics &&
                !canManageWarehouse)
            {
                return new HttpUnauthorizedResult();
            }

            if (plan.Status == PlanStatus.Closed)
            {
                return Content(String.Format("Logistics Plan {0} completed. Access to its packages not allowed!", plan.Id));
            }

            var nextStatuses = _logisticsProcessingService.GetNextStatuses(pkg);

            bool statusUpdated = false;
            if (canManageWarehouse && nextStatuses.Contains(ShippingStatus.InWarehouse))
            {
                _logisticsProcessingService.UpdateStatus(pkg, ShippingStatus.InWarehouse);
                statusUpdated = true;
            }
            else if (canManageWarehouse && nextStatuses.Contains(ShippingStatus.InWarehouseFailedDelivery))
            {
                _logisticsProcessingService.UpdateStatus(pkg, ShippingStatus.InWarehouseFailedDelivery);
                statusUpdated = true;
            }

            if (statusUpdated)
            {
                // If all packages in the plan has been serviced, 
                // then automatically mark the plan as completed.
                if (_logisticsService.LogisticsPlanCompleted(plan.Id))
                {
                    _logisticsService.MarkLogisticsPlanAsCompleted(plan.Id, customer.Id);
                    return Content(String.Format("Logistics Plan {0}: ({1}, {2}) has been completed.", plan.Id, plan.Courier.Customer.GetFullName(), plan.Courier.Customer.Email));
                }
                else
                {
                    SuccessNotification(String.Format("Package ({0}) has been changed to In HUB.", pkg.Id));
                    return RedirectToAction("PlannedRouteList", new { id = plan.Id });
                }
            }

            bool allowStatusUpdate = true;

            // prevent status being updated IF plan is not yet set to In-Progress
            if (plan.Status != PlanStatus.Running)
            {
                allowStatusUpdate = false;
            }

            // prevent status being updated IF package has not been properly ranked within the plan.
            // Package does not need ranking only when: ReturningWarehouse where warehouse is the same as plan's.
            if (!(pkg.Status == ShippingStatus.ReturningWarehouse && (!pkg.ToWarehouseId.HasValue || pkg.ToWarehouseId.Value == plan.WarehouseId.Value)) &&
                !_logisticsService.PackageRanked(pkg, planId))
            {
                allowStatusUpdate = false;
            }

            // check whether enough time has elapsed since last status change in order to allow another status change.
            // This is to prevent accidental consecutive status changes.
            if (!canManageLogistics && // the manager can always change status
                !_logisticsService.PackageStatusUpdateable(pkg, customer))
            {
                allowStatusUpdate = false;
            }

            #region Build view model

            var creationTime = _dateTimeHelper.ConvertToUserTime(pkg.CreatedOnUtc, DateTimeKind.Utc);
            var model = new LogisticsPackageModel()
            {
                Id = pkg.Id,
                PlanId = plan.Id,
                ShipmentId = pkg.ShipmentId,
                WarehouseId = pkg.WarehouseId,
                PickUpLocationId = pkg.PickUpLocationId,
                DeliveryLocationId = pkg.DeliveryLocationId,
                CreationTime = creationTime,
                UpdatedTime = pkg.UpdatedOnUtc.HasValue ? _dateTimeHelper.ConvertToUserTime(pkg.UpdatedOnUtc.Value, DateTimeKind.Utc) : creationTime,
                Status = (int)pkg.Status,
                TrackingNumber = pkg.Shipment.TrackingNumber,
                OrderBy = null
            };

            var orderInfo = "Unknown";

            try
            {
                orderInfo = String.IsNullOrWhiteSpace(pkg.Shipment.Order.OrderByInfo) ?
                    pkg.Shipment.Order.Customer.GetCompanyNameOrFullName() : pkg.Shipment.Order.OrderByInfo;
            }
            catch (Exception)
            {
                LogError(String.Format("Package.Shipment.Order.Customer cannot be retrieved. pkg# {0}", pkg.Id));
            }

            model.OrderBy = orderInfo;

            // initialize to null. Can only be non-null if pkg in the current plan.
            model.SamePickupLocationNumItems = null;
            model.SameDeliveryLocationNumItems = null;

            if (pkg.PickUpLocation.PlanId.HasValue &&
                pkg.PickUpLocation.PlanId.Value == planId &&
                _logisticsService.PackageAtPickupLocation(pkg))
            {
                model.SamePickupLocationNumItems = _logisticsService.GetPackagesByPickupLocation(planId, pkg.PickUpLocation, true).Count();
            }

            if (pkg.DeliveryLocation.PlanId.HasValue &&
                pkg.DeliveryLocation.PlanId.Value == planId &&
                pkg.Status != ShippingStatus.Delivered)
            {
                if (!pkg.ToWarehouseId.HasValue)
                {
                    model.SameDeliveryLocationNumItems = _logisticsService.GetPackagesByDeliveryLocation(planId, pkg.DeliveryLocation, true).Count();
                }
                else
                {
                    model.SameDeliveryLocationNumItems = _logisticsService.GetPackagesToWarehouse(plan, pkg.ToWarehouse).Count();
                }
            }

            // TODO: TBD: How is this used? What updates for multi-district/multi-warehouse?
            if (pkg.WarehouseId.HasValue)
            {
                model.WarehouseAddress = new AddressModel()
                {
                    Company = pkg.Warehouse.Address.Company,
                    FirstName = pkg.Warehouse.Address.FirstName,
                    LastName = pkg.Warehouse.Address.LastName,
                    Address1 = pkg.Warehouse.Address.Address1,
                    Address2 = pkg.Warehouse.Address.Address2,
                    City = pkg.Warehouse.Address.City,
                    StateProvinceName = pkg.Warehouse.Address.StateProvince.Name,
                    ZipPostalCode = pkg.Warehouse.Address.ZipPostalCode,
                    CountryName = pkg.Warehouse.Address.Country.Name,
                    Email = pkg.Warehouse.Address.Email,
                    CellPhoneNumber = pkg.Warehouse.Address.CellPhoneNumber,
                    PhoneNumber = pkg.Warehouse.Address.PhoneNumber,
                };
            }

            model.PickUpAddress = new AddressModel()
            {
                Company = pkg.PickUpLocation.Address.Company,
                FirstName = pkg.PickUpLocation.Address.FirstName,
                LastName = pkg.PickUpLocation.Address.LastName,
                Address1 = pkg.PickUpLocation.Address.Address1,
                Address2 = pkg.PickUpLocation.Address.Address2,
                City = pkg.PickUpLocation.Address.City,
                StateProvinceName = pkg.PickUpLocation.Address.StateProvince.Name,
                ZipPostalCode = pkg.PickUpLocation.Address.ZipPostalCode,
                CountryName = pkg.PickUpLocation.Address.Country.Name,
                Email = pkg.PickUpLocation.Address.Email,
                CellPhoneNumber = pkg.PickUpLocation.Address.CellPhoneNumber,
                PhoneNumber = pkg.PickUpLocation.Address.PhoneNumber,
            };

            // TODO: TBD: How is this used? What updates for multi-district/multi-warehouse?
            if (pkg.ToWarehouseId.HasValue)
            {
                model.DeliveryAddress = new AddressModel()
                {
                    Company = pkg.ToWarehouse.Address.Company,
                    FirstName = pkg.ToWarehouse.Address.FirstName,
                    LastName = pkg.ToWarehouse.Address.LastName,
                    Address1 = pkg.ToWarehouse.Address.Address1,
                    Address2 = pkg.ToWarehouse.Address.Address2,
                    City = pkg.ToWarehouse.Address.City,
                    StateProvinceName = pkg.ToWarehouse.Address.StateProvince.Name,
                    ZipPostalCode = pkg.ToWarehouse.Address.ZipPostalCode,
                    CountryName = pkg.ToWarehouse.Address.Country.Name,
                    Email = pkg.ToWarehouse.Address.Email,
                    CellPhoneNumber = pkg.ToWarehouse.Address.CellPhoneNumber,
                    PhoneNumber = pkg.ToWarehouse.Address.PhoneNumber,
                };
            }
            else
            {
                model.DeliveryAddress = new AddressModel()
                {
                    Company = pkg.DeliveryLocation.Address.Company,
                    FirstName = pkg.DeliveryLocation.Address.FirstName,
                    LastName = pkg.DeliveryLocation.Address.LastName,
                    Address1 = pkg.DeliveryLocation.Address.Address1,
                    Address2 = pkg.DeliveryLocation.Address.Address2,
                    City = pkg.DeliveryLocation.Address.City,
                    StateProvinceName = pkg.DeliveryLocation.Address.StateProvince.Name,
                    ZipPostalCode = pkg.DeliveryLocation.Address.ZipPostalCode,
                    CountryName = pkg.DeliveryLocation.Address.Country.Name,
                    Email = pkg.DeliveryLocation.Address.Email,
                    CellPhoneNumber = pkg.DeliveryLocation.Address.CellPhoneNumber,
                    PhoneNumber = pkg.DeliveryLocation.Address.PhoneNumber,
                };
            }

            model.Attribute = pkg.Shipment.Attributes.HasValue && (pkg.Shipment.Attributes.Value != 0) ?
                ((AddtionalAttributes)pkg.Shipment.Attributes.Value).GetLocalizedEnum(_localizationService, _workContext.WorkingLanguage.Id) : "";

            if (pkg.Shipment.GetTotalDim() > _logisticsInformationSettings.OversizeDimensionSize)
            {
                model.Attribute += "[OVERSIZE]";
            }

            model.Reference = pkg.Shipment.Comment;

            // display only latest note on driver's mobile display.
            var latestPkgNote = _logisticsService.GetPackageNotesByPackageId(pkg.Id, 1, new PackageNoteType[] { PackageNoteType.AdminNote }).SingleOrDefault();
            model.PackageNote = latestPkgNote != null ? latestPkgNote.Note : null;

            #endregion

            if (allowStatusUpdate &&
                (customer.Id == plan.Courier.Customer.Id || canManageWarehouse))
            {
                // address type options
                var addressTypes = new List<SelectListItem>();
                model.SelectedAddressTypeId = null;
                if (pkg.Status == ShippingStatus.ReadyForPickUp && !pkg.PickUpLocation.Address.VerifiedTypeId.HasValue)
                {
                    model.AvaliableAddressTypes =
                        ((AddressType)AddressType.Unknown).ToSelectList(true).Where(x => !x.Selected).ToList();
                    model.VerifyAddressId = pkg.PickUpLocation.Address.Id;
                }
                else if (pkg.Status == ShippingStatus.Delivering && !pkg.DeliveryLocation.Address.VerifiedTypeId.HasValue)
                {
                    model.AvaliableAddressTypes =
                        ((AddressType)AddressType.Unknown).ToSelectList(true).Where(x => !x.Selected).ToList();
                    model.VerifyAddressId = pkg.DeliveryLocation.Address.Id;
                }

                // failed delivery options
                if (nextStatuses.Contains(ShippingStatus.ReturningWarehouseFailedDelivery))
                {
                    nextStatuses.Remove(ShippingStatus.ReturningWarehouseFailedDelivery);

                    var packageNoteTemplates = _logisticsService.GetPackageNoteTemplatesByStatus(ShippingStatus.ReturningWarehouseFailedDelivery);
                    if (packageNoteTemplates.Count > 0)
                    {
                        model.FailedResponses = packageNoteTemplates.Select(
                                    x => new SelectListItem()
                                    {
                                        Text = x.SystemName,
                                        Value = x.Id.ToString(),
                                        Selected = false
                                    }).ToList();
                    }
                }

                // other status options
                model.StatusValues = nextStatuses
                    .Select(x => new SelectListItem() { Selected = false, Text = x.ToString(), Value = ((int)x).ToString() })
                    .ToList();
            }
            else
            {
                model.StatusValues = new List<SelectListItem>();
            }

            return View(model);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Status(LogisticsPackageModel model)
        {
            if (!_permissionService.Authorize(StandardPermissionProvider.LogisticsUpdateStatus) &&
                !_permissionService.Authorize(StandardPermissionProvider.ManageWarehouse) &&
                !_permissionService.Authorize(StandardPermissionProvider.ManageLogistics))
                return new HttpUnauthorizedResult();

            var customer = _workContext.CurrentCustomer;
            try
            {
                if (!ModelState.IsValid)
                {
                    return Content("Status cannot be changed.");
                }

                if (model.SelectedFailedResponse > 0)
                {
                    model.SelectedStatus = (int)ShippingStatus.ReturningWarehouseFailedDelivery;
                }

                if (model.SelectedStatus == -1)  // -1 if the status values for user selection is empty
                    return Content(String.Format("No status update to package ID {0} is performed.", model.Id));

                var pkg = _logisticsService.GetPackageById(model.Id);
                if (model.Status != (int)pkg.Status)
                {
                    return Content("No status is updated. Please re-scan the package.");
                }
                if (model.SelectedAddressTypeId.HasValue)
                {
                    if (!Enum.IsDefined(typeof(AddressType), model.SelectedAddressTypeId.Value) ||
                        ((AddressType)model.SelectedAddressTypeId) == AddressType.Unknown)
                        return Content("The selected address type is invalid.");

                    var verifyingAddr = _addressService.GetAddressById(model.VerifyAddressId);
                    if (verifyingAddr == null)
                        return Content("The address which is being verified is invalid.");

                    verifyingAddr.VerifiedTypeId = model.SelectedAddressTypeId.Value;
                    _addressService.UpdateAddress(verifyingAddr);
                }

                var plan = _logisticsService.PackageLatestPlan(pkg);  // should not be null, since we are here.

                if (model.SelectedFailedResponse > 0)
                {
                    var failDeliveryTemplate = _logisticsService.GetPackageNoteTemplateById(model.SelectedFailedResponse);
                    _logisticsProcessingService.FailDelivery(pkg, failDeliveryTemplate);
                }
                else
                {
                    _logisticsProcessingService.UpdateStatus(pkg, (ShippingStatus)model.SelectedStatus);
                }
                // If all packages in the plan has been serviced, 
                // then automatically mark the plan as completed.
                if (_logisticsService.LogisticsPlanCompleted(plan.Id))
                {
                    _logisticsService.MarkLogisticsPlanAsCompleted(plan.Id, customer.Id);
                }

                // TODO: Need to decide where to re-direct afterwards!
                //       For now, redirect to the same page to quickly check if status updated!
                //return RedirectToAction("Status", new { pid = model.Id });

                //return Content(String.Format("The status is updated to {0} successfully", pkg.Status));
                ViewBag.StatusConfirmString = String.Format("The status is updated to {0} successfully", pkg.Status);
                ViewBag.PlanId = plan.Id;
                ViewBag.CourierInfo = String.Format("{0} - {1}", customer.GetFullName(), customer.Email);
                return View("StatusConfirmation");
            }
            catch (Exception e)
            {
                LogException(e);
                return RedirectToAction("Status", new { id = model.Id });
            }
        }

        public ActionResult PlannedRouteList(int? id)
        {
            if (!_permissionService.Authorize(StandardPermissionProvider.ViewLogisticsRoutes))
                return new HttpUnauthorizedResult();

            var customer = _workContext.CurrentCustomer;

            LogisticsPlan plan = null;
            if (id.HasValue)
            {
                plan = _logisticsService.GetPlanByPlanId(id.Value);
                if (plan == null &&
                    !_permissionService.Authorize(StandardPermissionProvider.ManageLogistics) &&
                    !_permissionService.Authorize(StandardPermissionProvider.ManageWarehouse))
                {
                    return Content(String.Format("Cannot retrieve Logistics Plan with planId ({0})", id.Value));
                }
            }
            else
            {
                // try to get the default active plan for the driver by its driver Id
                plan = _logisticsService.GetPlanByDriverId(customer.Id);
                if (plan == null &&
                    !_permissionService.Authorize(StandardPermissionProvider.ManageLogistics) &&
                    !_permissionService.Authorize(StandardPermissionProvider.ManageWarehouse))
                {
                    return Content(String.Format("Cannot retrieve an active Logistics Plan with DriverID ({0}) or plan was completed.", customer.Id));
                }
            }

            if (plan != null &&
                customer.Id != plan.Courier.Customer.Id &&
                !_permissionService.Authorize(StandardPermissionProvider.ManageLogistics) &&
                !_permissionService.Authorize(StandardPermissionProvider.ManageWarehouse))
            {
                return new HttpUnauthorizedResult();
            }

            PlannedRouteModel model = null;
            try
            {
                if (!plan.MarkedDelivering)
                    _logisticsProcessingService.SetAllPackageDeliveringInPlan(plan);

                if (plan != null)
                {
                    model = PreparePlannedRouteModel(plan);
                    //model.MapContext = new MapContextModel();
                    //model.MapContext.ShowRoute = true;
                    //model.MapContext.SequentialLocations = GetLocationsByPlan(plan, false);
                }
                else
                {
                    model = new PlannedRouteModel();
                }

                if (_permissionService.Authorize(StandardPermissionProvider.ManageLogistics) ||
                    _permissionService.Authorize(StandardPermissionProvider.ManageWarehouse))
                {
                    // add info regarding available other plans for viewer with manage logistics permission
                    // TBI:

                    int? districtId = _customerService.GetDistrictId(customer);
                    //if (districtId == null)
                    //    return Content(String.Format("Unassigned districtID for customer ({0}).", customer.Id));

                    District district = null;
                    if (districtId.HasValue && districtId.Value != 0)
                    {
                        district = _logisticsService.GetDistrictById(districtId.Value);
                        if (district == null)
                            return Content(String.Format("Invalid distric ({0}).", districtId.Value));
                    }

                    // TODO: Needs to limit showing plans that is in the same district? or, have packages going to its district?
                    var availablePlanStatus = new[] { PlanStatus.Locked, PlanStatus.Running };
                    var availablePlans = _logisticsService.GetPlans(availablePlanStatus, null, null, null);

                    // show only plan accessible to the user.
                    if (district != null)
                    {
                        availablePlans = availablePlans.Where(pl => _logisticsService.PlanAccessibleToDistrict(pl, district)).ToList();
                    }

                    model.AvailablePlanIds.Add(new SelectListItem() { Text = "None", Value = "0", Selected = (plan == null) ? true : false });
                    foreach (var apl in availablePlans)
                    {
                        var availPlanDisplayString = String.Format("{0} ( {1} - {2} )", apl.Id.ToString(), apl.Courier.Customer.GetFullName(), apl.Courier.Customer.Email ?? String.Empty);
                        model.AvailablePlanIds.Add(new SelectListItem() { Text = availPlanDisplayString, Value = apl.Id.ToString(), Selected = (plan != null && plan.Id == apl.Id) ? true : false });
                    }
                }
            }
            catch (Exception exc)
            {
                return FatalErrorView(String.Format("Failed building view model for Planned Route with planId ({0})", plan != null ? plan.Id.ToString() : "--"), customer, exc);
            }

            return View(model);
        }

        [HttpPost]
        public ActionResult PlannedRouteList(PlannedRouteModel model)
        {
            var customer = _workContext.CurrentCustomer;
            try
            {
                if (!ModelState.IsValid)
                {
                    return FatalErrorView("Model is invalid", customer);
                }

                if (model.SelectedPlanId > 0)
                {
                    return RedirectToAction("PlannedRouteList", new { id = model.SelectedPlanId });
                }
            }
            catch (Exception e)
            {
                LogException(e);
                return FatalErrorView("Error processing HttpPost for Planned Route List", customer, e);
            }

            return View(model);
        }

        public ActionResult DownloadWaybill(int? id, string token)
        {
            if (!id.HasValue)
            {
                return AccessDeniedView();
            }

            var pkg = id.HasValue ? _logisticsService.GetPackageById(id.Value) : null;
            if (pkg == null)
            {
                return AccessDeniedView();
            }

            var savedToken = pkg.GetAttribute<string>(SystemLogisticsAttributeNames.WaybillDownloadToken, _genericAttributeService);
            if (String.IsNullOrEmpty(savedToken) || !savedToken.Equals(token, StringComparison.InvariantCultureIgnoreCase))
            {
                return AccessDeniedView();
            }

            //Download
            var trackingUrl = Url.RouteUrl("LogisticsTrack", new { pid = id.Value }, Request.Url.Scheme);

            byte[] bytes = null;
            using (var stream = new System.IO.MemoryStream())
            {
                _pdfService.PrintLogisticsWaybillToPdf(stream, pkg, trackingUrl, _workContext.WorkingLanguage);
                bytes = stream.ToArray();
            }
            return File(bytes, "application/pdf", WaybillFilename.GenerateWaybillFilename(pkg.Shipment.TrackingNumber));
        }

        public ActionResult ShowCourierCommission()
        {
            if (!_permissionService.Authorize(StandardPermissionProvider.ViewLogisticsRoutes))
                return new HttpUnauthorizedResult();

            var customer = _workContext.CurrentCustomer;

            var numDays = 60;

            var endDate = _dateTimeHelper.ConvertToUserTime(DateTime.UtcNow, DateTimeKind.Utc).Date.AddDays(1);
            var startDate = endDate.AddDays(-numDays - 1); // put one more day, just to be sure at least numDays back.

            var endDateUtc = _dateTimeHelper.ConvertToUtcTime(endDate, _dateTimeHelper.CurrentTimeZone);
            var startDateUtc = _dateTimeHelper.ConvertToUtcTime(startDate, _dateTimeHelper.CurrentTimeZone);

            var model = new CourierCommissionModel()
            {
                CourierId = customer.Id,
                CourierName = customer.GetFullName(),
                CourierEmail = customer.Email,
                StartDate = startDate,
                EndDate = endDate,
                TotalCommission = 0M,
            };

            var plans = _logisticsService.GetPlans(new PlanStatus[] { PlanStatus.Closed },
                                                   startDateUtc, endDateUtc, model.CourierId);

            if (plans != null && plans.Count() > 0)
            {
                // There are actually plans returned on which we can compute commission
                decimal commissionRate = _logisticsInformationSettings.CourierCommissionRate;

                foreach (var plan in plans)
                {
                    var planNotes = _logisticsService.GetPlanNotesForComissionByPlanId(plan.Id);

                    var commissionPlanModel = new CourierCommissionPlanModel()
                    {
                        PlanId = plan.Id,
                        CourierCommission = planNotes.Count() * commissionRate,
                    };

                    model.Plans.Add(commissionPlanModel);

                    model.TotalCommission += commissionPlanModel.CourierCommission;
                }
            }

            return View(model);
        }

        //public ActionResult Track(string trackingNumber)
        //{
        //    var model = new PackageTrackingModel();
        //    model.Notifications = new List<string>();
        //    if (!String.IsNullOrWhiteSpace(trackingNumber))
        //    {
        //        var pkg = _logisticsService.SearchPackages(
        //            0,
        //            null,
        //            null,
        //            null,
        //            null,
        //            trackingNumber,
        //            null,
        //            null,
        //            null,
        //            null,
        //            null,
        //            null,
        //            null,
        //            null,
        //            0,
        //            20).FirstOrDefault<LogisticsPackage>();
        //        if (pkg != null && pkg.ShipmentId > 0)
        //        {
        //            model.TrackingNumber = pkg.Shipment.TrackingNumber;
        //            model.CurrentStatus = pkg.Status;
        //            model.PickUpAddress = PrepareAddressModel(pkg.PickUpLocation.Address);
        //            model.DeliveryAddress = PrepareAddressModel(pkg.DeliveryLocation.Address);
        //            model.StatusEvents = new List<ShipmentStatusEvent>();
        //            string shippingMethod = "Shipping.Spotshub";
        //            var srcm = _shippingService.LoadShippingRateComputationMethodBySystemName(shippingMethod);
        //            if (srcm != null && srcm.ShipmentTracker != null)
        //            {
        //                try
        //                {
        //                    var shipmentEvents = srcm.ShipmentTracker.GetShipmentEvents(trackingNumber);
        //                    if (shipmentEvents != null && shipmentEvents.Count > 0)
        //                    {
        //                        shipmentEvents.OrderByDescending(s => s.DateTime);
        //                        model.StatusEvents = shipmentEvents;
        //                    }

        //                }
        //                catch (Exception e)
        //                {
        //                    model.Notifications.Add(e.Message);
        //                    model.Notifications.Add("We encountered problems trying to render package events. Please try again late.");
        //                }
        //            }
        //            else
        //            {
        //                model.Notifications.Add("Invalid Package Information.");
        //            }

        //        }
        //        else
        //        {
        //            model.Notifications.Add("Package does not exist, or you do not have the privilege to view this package.");
        //        }
        //    }
        //    return View(model);
        //}


        public ActionResult PackageTracking(string trackingNumber)
        {
            var model = new PackageTrackingModel();
            model.Notifications = new List<string>();
            if (!String.IsNullOrWhiteSpace(trackingNumber))
            {
                var pkg = _logisticsService.SearchPackages(
                    0,
                    null,
                    null,
                    null,
                    null,
                    trackingNumber,
                    null,
                    null,
                    null,
                    null,
                    null,
                    null,
                    null,
                    null,
                    0,
                    20).FirstOrDefault<LogisticsPackage>();
                if (pkg != null && pkg.ShipmentId > 0)
                {
                    model.TrackingNumber = pkg.Shipment.TrackingNumber;
                    model.CurrentStatus = pkg.Status;
                    model.PickUpAddress = PrepareAddressModel(pkg.PickUpLocation.Address);
                    model.DeliveryAddress = PrepareAddressModel(pkg.DeliveryLocation.Address);
                    model.StatusEvents = new List<ShipmentStatusEvent>();
                    string shippingMethod = "Shipping.Spotshub";
                    var srcm = _shippingService.LoadShippingRateComputationMethodBySystemName(shippingMethod);
                    if (srcm != null && srcm.ShipmentTracker != null)
                    {
                        try
                        {
                            var shipmentEvents = srcm.ShipmentTracker.GetShipmentEvents(trackingNumber);
                            if (shipmentEvents != null && shipmentEvents.Count > 0)
                            {
                                shipmentEvents.OrderByDescending(s => s.DateTime);
                                model.StatusEvents = shipmentEvents;
                            }

                        }
                        catch (Exception e)
                        {
                            model.Notifications.Add(e.Message);
                            model.Notifications.Add("We encountered problems trying to render package events. Please try again late.");
                        }
                    }
                    else
                    {
                        model.Notifications.Add("Invalid Package Information.");
                    }

                }
                else
                {
                    model.Notifications.Add("Package does not exist, or you do not have the privilege to view this package.");
                }
            }
            return View(model);
        }

        [HttpPost]
        public ActionResult GetShipmentIdByTrackingNumber(string trackingNumber)
        {
            try
            {
                var shipment = _logisticsService.GetShipmentByTrackingNumber(trackingNumber);
                if (shipment != null)
                    return Json(new { ShipmentId = shipment.Id });
            }
            catch (Exception ex)
            {
                _logger.Error(String.Format("Search tracking number {0}.", trackingNumber), ex);
            }
            return Json(new { Error = _localizationService.GetResource("Logistics.Tracking.TrackingNumberNotFound") });
        }

        [ChildActionOnly]
        public ActionResult TrackForm()
        {
            ViewBag.TrackingNumberRegex = _logisticsInformationSettings.TrackingNumberRegex;
            return View();
        }

        #endregion

        #region Utilities

        [NonAction]
        private PlannedRouteModel PreparePlannedRouteModel(LogisticsPlan plan)
        {
            var model = new PlannedRouteModel();

            if (plan != null)
            {
                model.PlanId = plan.Id;

                var locs = _logisticsService.GetLogisticsLocationsByPlanId(plan.Id);

                var ranks = locs.Where(l => l.Rank.HasValue).Select(l => l.Rank.Value);
                var bigRank = 10000;
                if (ranks.Count() > 0)
                {
                    bigRank = bigRank + ranks.Max();
                }

                locs = locs.Where(x => x.Package.Status != ShippingStatus.FailedPickUp
                    && x.Package.Status != ShippingStatus.Delivered).ToList();

                model.PackageLocations = locs
                    .Where(x => x.Package.Status != ShippingStatus.FailedPickUp && x.Package.Status != ShippingStatus.Delivered)
                    .Select(x =>
                {
                    try
                    {
                        var pkgLocModel = new PackageLocationModel()
                        {
                            LocationId = x.Id,
                            AddressId = x.AddressId,
                            UpdatedOnUtc = x.UpdatedOnUtc.HasValue ? (DateTime?)x.UpdatedOnUtc.Value : null,
                            Rank = x.Rank.HasValue ? (decimal?)x.Rank.Value : bigRank++,
                            LocationStatus = x.Status,
                            Action = x.Id == x.Package.PickUpLocationId ? "Pick Up" : "Delivery",
                            PlanId = x.PlanId.Value,        // must exist
                            CourierId = x.CourierId.Value,  // must exist
                            PackageId = x.PackageId.Value,  // must exist
                            ShippingStatus = x.Package.Status,
                            AddressZone = _logisticsService.GetZoneByZipPostalCode(x.Address.ZipPostalCode,
                                                                                   x.Address.Country.TwoLetterIsoCode).Name,
                            AddressType = x.Address.Type.HasValue ? x.Address.Type.Value.GetLocalizedEnum(_localizationService, _workContext) : "Unknown",
                        };

                        bool isFailDelivery = _logisticsService.GetFailedDeliveryEventCount(x.Package) > 0;
                        bool isFinalDelivery = _logisticsService.IsMaxDeliveryAttempt(x.Package);
                        if (isFailDelivery || isFinalDelivery)
                        {
                            string newString = isFinalDelivery ? " [FF]" : " [F1]";
                            pkgLocModel.Action += newString;
                        }

                        // initialize to null. 
                        pkgLocModel.SamePickupLocationNumItems = null;
                        pkgLocModel.SameDeliveryLocationNumItems = null;

                        if ((x.Id == x.Package.PickUpLocationId) &&
                            _logisticsService.PackageAtPickupLocation(x.Package))
                        {
                            pkgLocModel.SamePickupLocationNumItems = _logisticsService.GetPackagesByPickupLocation(x.PlanId.Value, x, true).Count();
                        }

                        if ((x.Id == x.Package.DeliveryLocationId) &&
                            x.Package.Status != ShippingStatus.Delivered)
                        {
                            if (!x.Package.ToWarehouseId.HasValue)
                            {
                                pkgLocModel.SameDeliveryLocationNumItems = _logisticsService.GetPackagesByDeliveryLocation(x.PlanId.Value, x, true).Count();
                            }
                            else
                            {
                                pkgLocModel.SameDeliveryLocationNumItems = _logisticsService.GetPackagesToWarehouse(x.Plan, x.Package.ToWarehouse).Count();
                            }
                        }

                        // Need to build addressModel inside pkgLocModel
                        if ((x.Id == x.Package.DeliveryLocationId) && x.Package.ToWarehouseId.HasValue)
                        {
                            // populate address with that of ToWarehouse, if this is delivery to a warehouse.
                            pkgLocModel.Address.Address1 = x.Package.ToWarehouse.Address.Address1;
                            pkgLocModel.Address.Address2 = x.Package.ToWarehouse.Address.Address2;
                            pkgLocModel.Address.City = x.Package.ToWarehouse.Address.City;
                            pkgLocModel.Address.StateProvinceId = x.Package.ToWarehouse.Address.StateProvinceId;
                            pkgLocModel.Address.StateProvinceName = x.Package.ToWarehouse.Address.StateProvince.Name;
                            pkgLocModel.Address.ZipPostalCode = x.Package.ToWarehouse.Address.ZipPostalCode;
                            pkgLocModel.Address.CountryId = x.Package.ToWarehouse.Address.CountryId;
                            pkgLocModel.Address.CountryName = x.Package.ToWarehouse.Address.Country.Name;
                            pkgLocModel.Address.PhoneNumber = x.Package.ToWarehouse.Address.PhoneNumber;
                            pkgLocModel.Address.CellPhoneNumber = x.Package.ToWarehouse.Address.CellPhoneNumber;
                        }
                        else
                        {
                            pkgLocModel.Address.Address1 = x.Address.Address1;
                            pkgLocModel.Address.Address2 = x.Address.Address2;
                            pkgLocModel.Address.City = x.Address.City;
                            pkgLocModel.Address.StateProvinceId = x.Address.StateProvinceId;
                            pkgLocModel.Address.StateProvinceName = x.Address.StateProvince == null ? null : x.Address.StateProvince.Name;
                            pkgLocModel.Address.ZipPostalCode = x.Address.ZipPostalCode;
                            pkgLocModel.Address.CountryId = x.Address.CountryId;
                            pkgLocModel.Address.CountryName = x.Address.Country == null ? null : x.Address.Country.Name;
                            pkgLocModel.Address.PhoneNumber = x.Address.PhoneNumber;
                            pkgLocModel.Address.CellPhoneNumber = x.Address.CellPhoneNumber;
                        }

                        // Currently locationStatus is not really being actively updated.
                        // IF it is, then only the first condition below is needed.
                        // For now, we actually need to deduce a location's status based on the package's 
                        // overall status possibilites.
                        // TBI:
                        var pkgStatus = x.Package.Status;
                        if ((pkgLocModel.LocationStatus == LocationStatus.Complete) ||
                            (x.Id == x.Package.DeliveryLocationId &&
                             (_logisticsService.PackageDeliveryAttempted(x.Package) ||
                              pkgStatus == ShippingStatus.FailedPickUp)) ||
                            (x.Id == x.Package.PickUpLocationId && _logisticsService.PackagePickupAttempted(x.Package)))
                        {
                            pkgLocModel.Action += " (Completed)";
                        }
                        else if (x.Id == x.Package.DeliveryLocationId && (pkgStatus == ShippingStatus.ReturningWarehouse || pkgStatus == ShippingStatus.ReturningWarehouseFailedDelivery))
                        // The loction is delivery location but the package was set to return warehouse, so set as completed
                        {
                            pkgLocModel.Action += "(Abort)(Completed)";
                        }

                        pkgLocModel.Attribute = x.Package.Shipment.Attributes.HasValue && (x.Package.Shipment.Attributes.Value != 0) ?
                            ((AddtionalAttributes)x.Package.Shipment.Attributes.Value).GetLocalizedEnum(_localizationService, _workContext.WorkingLanguage.Id) : "";

                        if (x.Package.Shipment.GetTotalDim() > _logisticsInformationSettings.OversizeDimensionSize)
                        {
                            pkgLocModel.Attribute += "[OVERSIZE]";
                        }

                        pkgLocModel.Reference = x.Package.Shipment.Comment;

                        return pkgLocModel;
                    }
                    catch (Exception ex)
                    {
                        throw new NopException("Location model for location {0} of package {1} cannot be created. Exception: {2}", x.Id, x.PackageId, ex);
                    }
                }).ToList();
            }
            else
                throw new ArgumentNullException("plan");

            return model;
        }

        [NonAction]
        protected AddressModel PrepareAddressModel(Address address, Region region = null)
        {
            var model = new AddressModel();
            if (address == null)
            {
                address = new Address();
            }
            model.Company = address.Company != null ? address.Company : "";
            model.FirstName = address.FirstName != null ? address.FirstName : "";
            model.LastName = address.LastName != null ? address.LastName : "";
            model.Address1 = address.Address1 != null ? address.Address1 : "";
            model.Address2 = address.Address2 != null ? address.Address2 : "";
            model.City = address.City != null ? address.City : "";

            if (address.Type.HasValue && address.Type.Value != AddressType.Unknown)
            {
                model.Type = address.Type.Value;
            }

            //TODO: remove hard code for province
            if (address.StateProvince != null)
            {
                model.StateProvinceId = address.StateProvinceId;
                model.StateProvinceName = address.StateProvince.GetLocalized(x => x.Name);
            }
            else if (region != null)
            {
                var p = AddressHelper.GetProvinceByRegion(region);
                model.StateProvinceId = p.Id;
                model.StateProvinceName = p.GetLocalized(x => x.Name);
            }
            else
            {
                model.StateProvinceName = address.StateProvince == null ? AddressHelper.GetDefaultProvinceName() : address.StateProvince.Name;
                model.StateProvinceId = address.StateProvinceId != null ? address.StateProvinceId : 0;
            }

            model.ZipPostalCode = address.ZipPostalCode != null ? address.ZipPostalCode : "";

            //TODO: remove hard code for country
            model.CountryName = address.Country == null ? AddressHelper.GetDefaultCountryName() : address.Country.Name;
            model.CountryId = address.CountryId != null ? address.CountryId : 0;
            model.Email = address.Email != null ? address.Email : "";
            model.CellPhoneNumber = address.CellPhoneNumber != null ? address.CellPhoneNumber : "";
            model.PhoneNumber = address.PhoneNumber != null ? address.PhoneNumber : "";
            return model;
        }

        [NonAction]
        private List<MapLocationModel> GetLocationsByPlan(LogisticsPlan plan, bool needVerify = true)
        {
            const string warehouse = "Warehouse";
            const string completed = "Completed";
            const string pickup = "PickUp";
            const string delivery = "Delivery";

            var locs = GetSortedLocationsByPlanId(plan.Id, needVerify);

            if (locs == null)
            {
                throw new InvalidOperationException(String.Format("The logistics plan ({0}) cannot be found.", plan.Id));
            }

            var seqLocs = new List<MapLocationModel>();
            var whlm = new MapLocationModel()
            {
                Id = PlannedRouteModel.WarehouseFakeLocationId,
                Attribute = warehouse,
                Rank = null,
                PackageId = 0,
                City = plan.Warehouse.Address.City,
                ZipPostalCode = plan.Warehouse.Address.ZipPostalCode,
                AddressId = plan.Warehouse.Address.Id,
                Address = plan.Warehouse.Address.FormattedAddress()
            };

            seqLocs.Add(whlm);
            foreach (var loc in locs)
            {
                var lm = new MapLocationModel()
                {
                    Id = loc.Id,
                    Rank = loc.Rank,
                    PackageId = loc.PackageId.Value,
                    City = loc.Address.City,
                    ZipPostalCode = loc.Address.ZipPostalCode,
                    AddressId = loc.AddressId,
                    Address = loc.Address.FormattedAddress()
                };

                bool isPickup = loc.Id == loc.Package.PickUpLocationId;

                if (isPickup)
                {
                    lm.Attribute = pickup;
                    //lm.Status = loc.Package.Status == ShippingStatus.Delivering ? completed : string.Empty;
                    lm.Status = _logisticsService.PackagePickedUp(loc.Package) ? completed : string.Empty;
                }
                else
                {
                    lm.Attribute = delivery;
                    lm.Status = loc.Package.Status == ShippingStatus.Delivered || loc.Package.Status == ShippingStatus.ReturningWarehouse || loc.Package.Status == ShippingStatus.ReturningWarehouseFailedDelivery ? completed : string.Empty;
                }

                seqLocs.Add(lm);
            }
            seqLocs.Add(whlm);

            return seqLocs;
        }

        [NonAction]
        private List<LogisticsLocation> GetSortedLocationsByPlanId(int planId, bool needVerify)
        {
            if (_logisticsService.GetPlanByPlanId(planId) == null)
            {
                return null;
            }

            var locations = _logisticsService.GetLogisticsLocationsByPlanId(planId);

            if (needVerify)
            {
                if (locations == null || locations.Count() == 0)
                {
                    throw new InvalidOperationException("No locations are assigned to the plan.");
                }

                var temp = locations.Where(l => !l.Rank.HasValue);
                if (temp != null && temp.Count() != 0)
                {
                    var errLocs = new System.Text.StringBuilder();
                    errLocs.Append("Following location IDs (package IDs) are not ranked: ");
                    temp.ToList().ForEach(l => errLocs.Append(String.Format("{0} ({1}), ", l.Id, l.PackageId)));
                    throw new InvalidOperationException(errLocs.ToString());
                }

                var temp2 = locations.Where(l => !l.PackageId.HasValue);
                if (temp2 != null && temp2.Count() != 0)
                {
                    var errLocs = new System.Text.StringBuilder();
                    errLocs.Append("Following location IDs don't have associated package IDs:");
                    temp2.ToList().ForEach(l => errLocs.Append(String.Format("{0}, ", l.Id)));
                    throw new InvalidOperationException(errLocs.ToString());
                }
            }

            return locations;
        }

        #endregion
    }
}
